feat: Phase 2 Wave 1 — protocol slimming, answer-first, progressive retrieval, pinned core memory - #5
Merged
Merged
Conversation
…filter, mem_* audit log
…est audit passthrough, document audit env
…-set The slim protocol path was unreachable: protocolVersionFloor was 1.4.0 while this fork versions in the 0.x line, so `engram protocol-mode <slug>` always printed "full" and the session-start hook never rendered the compact protocol text. - Lower protocolVersionFloor to 0.4.0 (fork versioning). - Treat the "dev" build placeholder as meeting the floor (source builds are by definition current), and strip semver pre-release/build metadata before parsing so Go >= 1.24 VCS pseudo-versions (0.4.1-0.2026...+dirty) parse instead of silently degrading to "full". - Add `engram protocol-mode <slug> --set slim|full`, which persists via the existing setup.WriteProtocolMode and then prints the effective mode. An invalid value is fatal (unlike `setup --protocol=`, where it only warns). - Retune the pre-existing floor tests to the new 0.4.0 floor and dev-meets- floor semantics.
…bs conflict-loop detail
… rule in the budget test
…in full-branch rules in golden test
…paction full branch, align docs
…1 measures agent-visible payload mem_search no longer renders per-hit bodies. Hits render as a budgeted index (one line per hit: type, title, first-sentence summary, sync_id, approximate expansion cost, score) via the new FormatSearchIndex; agents scan the index and expand deliberately through mem_get_observation. New optional `budget` argument caps the index size (default 600 tokens); dropped hits are announced with an explicit "more omitted" marker instead of being silently lost. The response envelope is unchanged: count header, project/detection metadata, and the structured `results` array (which still lists every hit) all stay, as do the per-hit relation annotation lines (REQ-012), now interleaved after each shown index line. eval/runner L1 measures avg_tokens_per_query from FormatSearchIndex instead of a JSON dump of the result rows, so the metric tracks what the agent actually pays for.
…load, summary sentence guard
…ends) FormatContext now renders one layered block on both backends: ## Memory Context ### Pinned (as before: content cut to 300 runes) ### Recent Sessions (max 5, one line: <started_at> <first sentence>) ### Recent Observations (max 3, title + first sentence, 160-rune cap) Full bodies: mem_search / mem_get_observation. The whole block is hard capped at 1600 bytes (~400 tokens): overflow drops the oldest observation lines first, then the oldest session lines. Pinned content is never dropped (capping the pinned block itself is a separate change). Behaviour changes beyond rendering: - The "### Recent User Prompts" section is gone — prompts cost more bytes than they buy inside a 400-token budget, and they have no MemoryLake analogue, so keeping them would break cross-backend layout parity. They remain available via RecentPrompts / SearchPrompts (GET /prompts/recent, GET /prompts/search). FormatContext no longer reads user_prompts at all. - Store heading changed from "## Memory from Previous Sessions" and the MemoryLake heading from "## Memory Context (MemoryLake)" to the shared "## Memory Context" so both backends emit byte-identical structure. firstSentence is copied into internal/store and internal/memorylake with a comment naming internal/mcp.firstSentence as the canonical twin: internal/mcp imports internal/store, so store cannot import it back, and memorylake must not import mcp (import cycle). Same duplication pattern already used for truncate and approxTokens. Layout parity is enforced by the two mirrored unit tests (store.TestFormatContextLayeredBudget and memorylake.TestBackend_FormatContext_LayeredBudget): paritytest's corpus schema (CorpusEntry) has no context dimension — it describes observations to save plus gold queries — so a corpus row cannot express a context case, and `go test -tags parity ./internal/paritytest/` skips here (no live ENGRAM_MEMORYLAKE_API_KEY configured in this sandbox). Updated assertions that encoded the old layout: cmd/engram (heading), internal/server e2e (heading), store's prompts-section assertion (now asserts absence), store's "recent prompts error" subtest (now asserts FormatContext survives a dropped user_prompts table), and memorylake's 300-rune truncation test (retargeted at a pinned fact + a 160-rune summary assertion for the unpinned one).
…nd prompts posture Wave1 Task 9 review fixes. Tests: the store byte-cap test rendered 1473 bytes (under the 1600 cap), so the cap loop never ran and its drop-order assertions were vacuous. Both backends now use fixtures that overflow before dropping (store 1693, memorylake 1654), guard that fact explicitly via a reconstructed pre-cap render, and assert survivors by name: sessions all survive, newest observations survive, the oldest rendered one is dropped, pinned and footer never dropped. Section order (Pinned < Recent Sessions < Recent Observations) asserted on both sides. Validated against three mutations (sessions-before-observations, newest-first drop, cap loop deleted) -- each fails the store test. Code: sessionSummaryLine names the project in cross-project renders only (blank project filter, i.e. scope=personal), memorylake's formatContextContentTruncateLen renamed to contextPinnedTruncLen for grep parity with internal/store, and handleContext notes that contextByteCap bounds the rendered block only -- the stats/nudge tail is uncapped and outside the L2 measurement. Docs: DOCS.md no longer implies stdio agents can read prompts back; states the Wave-1 posture (write-only over MCP, readable via engram serve HTTP and the cloud dashboard) and documents the cross-project session line format.
The pinned section is the one part of the layered mem_context block the
1600-byte outer cap never drops, so an over-pinning project could starve
sessions/observations out of the block from the inside. Give it its own
ceiling.
Both backends now render `### Pinned (core memory)` with:
- a 1024-byte cap over the whole section (heading + entries + marker +
separator), enforced before the outer contextByteCap runs, so the outer
invariant ("never drops pinned") is unchanged;
- entries in pin-time ASCENDING order, keeping the section a stable prefix
that grows at the end as new pins arrive;
- overflow that drops the OLDEST pins from *rendering* only (nothing is
unpinned or deleted) behind a trailing
`(pin cap reached: N pinned facts not shown — mem_unpin to prune)`.
Pin time is a documented proxy, not a stored timestamp: neither backend
records when a pin happened (pinning deliberately does not touch
updated_at on SQLite), so ordering uses updated_at falling back to
created_at, with id breaking ties. Documented on PinnedObservations,
factPinTime and in DOCS.md.
Protocol text gains the pin criterion — "only facts whose loss causes
irreversible damage or repeated pitfalls" — in all four slim heredocs
(kept byte-identical, slim is now 1399/3400 bytes) and both memory
SKILL.md files, so an agent learns the cost of pinning in the same
session it can pin.
…text lines capPinnedLines fell through its drop loop when the pinned section reduced to a single entry that alone exceeded pinnedSectionByteCap, returning that line plus "(pin cap reached: 0 pinned facts not shown - mem_unpin to prune)". Nothing was withheld, so the marker was false agent-facing text. A lone entry now returns early: core memory is never emptied and no marker is emitted, so the marker's count is always >= 1. Also cap rendered titles at 120 runes (contextTitleTruncLen) in the pinned and recent-observation line renderers of both backends. Content was already bounded, but a pinned line is the one line neither cap may drop, so an unbounded title was the remaining way to carry the whole block past contextByteCap with nothing left to trim. Tests: per-backend coverage for the lone-over-cap entry (no marker anywhere) and for a 300-byte title no longer pushing a pinned-intact block past the 1600-byte outer cap, each with a guard proving the path is exercised. Adds a pin-cap sanity guard to the ML byte-cap fixture, notes the blank-line caveat on the pinnedSectionBody test helpers, and documents the two edge cases in DOCS.md and both memory SKILLs.
…he wire envelope convention
…s; honest wave1 report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4
Summary
Phase 2 Wave 1 of the memory eval & optimization program: five optimization rounds executed as an autonomous change→measure→gate loop, all kept, zero reverts. 27 commits, every task reviewed + fixed + re-reviewed, final whole-branch review on the strongest model.
What shipped
evalrun comparesuite,-tasksfilter with partial-scorecard guard,ENGRAM_MCP_AUDIT_LOG(all 22 mem_* tools)--set), compact 1265B slim heredoc, MCP instructions 3147B→736B (detail moved to on-demand SKILL)Corrected acceptance (read eval/results/wave1-report.md)
The final review caught an infrastructure artifact: 12 no-memory L3 runs (both baseline and final) had died on OAuth expiry and scored 0, inflating the program's uplift anchor. After checkpoint re-runs: true baseline uplift −0.367, Wave 1 −0.183. Defensible wins: injected tokens −22% harness / −50% real-session at flat recall@5 0.796; memory-arm e2e mean +0.967; arch drag cured (−0.90→+0.75); memory-arm verbosity +48%→+9%. The original ×2 target is void (anchored on the artifact); Wave 2 mandates (task-set redesign for memory-only knowledge, re-anchored goals, harness auth guard) are in the report.
Behavior notes for reviewers
engram protocol-mode <slug> --set slim); full remains default until a v0.4.0 release decision🤖 Generated with Claude Code